﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
using XcoAppSpaces.Core;

namespace SharedTextEditor
{

    public static class StringHelper
    {
        public static string ConvertToString(this Paragraph paragraph)
        {
            if (paragraph == null)
            {
                return String.Empty;
            }

            var inlines = paragraph.Inlines;

            var stringBuilder = new StringBuilder("");

            foreach (var inline in inlines)
            {
                var run = inline as Run;

                if (run != null)
                {
                    stringBuilder.Append(run.Text);
                  //  stringBuilder.Append(Environment.NewLine);
                }
            }

            return stringBuilder.ToString();
        }
    }

    /* TODO List
     *  + Send lock lines when selection/cursor changed
     *  + Unlock lines when user doesn't change anything for a few seconds ?!
     *  + Mark locked lines in GUI
     *  + Only allow editing when lines are not locked
     *  + Send updated lines (send on any change or cash and send every few seconds?)
     *  + Update document with updated lines
     * 
     * */
    public partial class MainWindow : Window
    {
        private BroadCast broadCast;
        private IList<CommunicationPartner> partners;
        private Document document;
        private DocumentServer docServer;
        private DocumentClient docClient;
        private Guid myId;

        // for GUI synchronization
        private readonly SynchronizationContext syncContext;

        private DispatcherTimer timer;

        public MainWindow()
        {
            // cstr must be called from main ui thread
            syncContext = SynchronizationContext.Current;

            InitializeComponent();

            partners = new SynchronizedCollection<CommunicationPartner>();
            myId = Guid.NewGuid();
            broadCast = new BroadCast(partners, myId);
            broadCast.NewPartnerFound += UpdateEditorCount;

            docServer = new DocumentServer();

            docClient = new DocumentClient(partners);
            docClient.PartnerDisconnected += UpdateEditorCount;
            StartCheckingForActivePartners();


            docServer.Document = new Document("Test Title", defaultText);

            docServer.DocumentChanged += DocServerOnDocumentChanged;

            TextInput.KeyDown += TextInputOnKeyDown;
            TextInput.PreviewKeyDown += TextInputOnPreviewKeyDown;
            TextInput.TextChanged += TextInput_TextChanged;
            DocServerOnDocumentChanged(docServer.Document);

            timer = new DispatcherTimer();
            timer.Interval = TimeSpan.FromSeconds(5);
            timer.Tick += (sender, args) =>
            {
                // release previous locks
                docClient.UnlockPrevious(partners);

                // stop timer
                timer.Stop();
            };
        }


        private string GetCurrentLine()
        {
            string h1 = TextInput.CaretPosition.GetTextInRun(LogicalDirection.Backward);
            string h2 = TextInput.CaretPosition.GetTextInRun(LogicalDirection.Forward);


            return h1 + h2;
        }

        private DocumentLine GetCurrentDocumentLine()
        {
            foreach (var line in docServer.Document.Lines)
            {
                if (line.Text == GetCurrentLine())
                {
                    return line;
                }
            }

            return null;
        }

        private string GetDocumentAsString()
        {
            string richText = new TextRange(TextInput.Document.ContentStart, TextInput.Document.ContentEnd).Text;

            return richText;
        }

        private string[] GetDocumentAsStringArray()
        {
            string richText = new TextRange(TextInput.Document.ContentStart, TextInput.Document.ContentEnd).Text;

            return richText.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
        }


        private object guiHandler = new object();

        private void DocServerOnDocumentChanged(Document document)
        {
            syncContext.Post(o =>
            {

                lock (guiHandler)
                {


                    FlowDocument flowDocument = new FlowDocument();

                    Style style = new Style(typeof(Paragraph));
                    style.Setters.Add(new Setter(Block.MarginProperty, new Thickness(0)));
                    flowDocument.Resources.Add((typeof(Paragraph)), style);
                    


                    var lines = new List<DocumentLine>(document.Lines);


                    foreach (var line in lines)
                    {
                        var paragraph = new Paragraph();
                        var span = new Span();


                        if (line.Locked)
                        {
                            paragraph.Background = Brushes.Gray;
                            paragraph.Style = style;
                        }

                        span.Inlines.Add(new Run(line.Text));


                        paragraph.Inlines.Add(span);
                        flowDocument.Blocks.Add(paragraph);
                    }




                    TextInput.Document = flowDocument;
                }
            }, null);


        }

        private bool deletingKey = false;

        private void TextInputOnKeyDown(object sender, KeyEventArgs keyEventArgs)
        {
            if (keyEventArgs.Key == Key.Delete)
            {
                deletingKey = true;
            }
            else
            {
                deletingKey = false;
            }

            
        }


        private void TextInputOnPreviewKeyDown(object sender, KeyEventArgs keyEventArgs)
        {
            var line = GetCurrentDocumentLine();

            if (line != null)
            {
                if (line.Locked)
                {
                    // prevent input --> cause line is locked!
                    keyEventArgs.Handled = true;
                }
            }

        }

        void TextInput_TextChanged(object sender, TextChangedEventArgs e)
        {
            // current caret position
            // TextInput.CaretPosition.
            DependencyObject adjDependencyObject = TextInput.CaretPosition.GetAdjacentElement(LogicalDirection.Forward);

            var result = TextInput.CaretPosition.GetPositionAtOffset(0, LogicalDirection.Forward);


            var run = TextInput.CaretPosition.GetAdjacentElement(LogicalDirection.Forward) as Run;

            if (run != null)
            {
                var paragraph = run.Parent as Paragraph;

                var paragraphContent = paragraph.ConvertToString();


                int lineNumber = 0;

                // find out paragraph position
                foreach (var block in TextInput.Document.Blocks)
                {
                    if (block == paragraph)
                    {
                        // found!
                        // out of the loop
                        break;
                    }

                    lineNumber++;
                }


                
                // in this case it's a new inserted line between --> search after previous Element
                if (lineNumber == (TextInput.Document.Blocks.Count() - 1))
                {
                    paragraph = paragraph.PreviousBlock as Paragraph;


                    // reset counter!
                    lineNumber = 0;

                    // find out paragraph position
                    foreach (var block in TextInput.Document.Blocks)
                    {
                        if (block == paragraph)
                        {
                            // found!
                            // out of the loop
                            break;
                        }

                        lineNumber++;
                    }
                }
               

                if (partners.Count > 0)
                {
                    timer.Stop();
                    timer.Start();
                    
                    // in that case a new line was inserted!
                    if (paragraphContent == String.Empty && deletingKey == false)
                    {
                        return;
                    }

                    if (paragraphContent.Count() == 0)
                    {
                        // add extra char to prevent missunderstooding with new line!
                        paragraphContent += " ";
                    }
                    

                    docClient.UnlockPrevious(partners);

                    var changedLines = new Dictionary<int, string>();
                    changedLines.Add(lineNumber, paragraphContent);

                    // update
                    docClient.ChangedLines(partners, changedLines);
                    
                    // then lock (updated) line
                    docClient.LockUnlockLines(partners, new List<int>() { lineNumber }, true);

                }


            }

            Console.WriteLine(result);


        }





        private void UpdateEditorCount(object sender, EventArgs e)
        {
            Application.Current.Dispatcher.Invoke(() => //execute with main thread
            {
                txtNrOfEditors.Text = (partners.Count + 1).ToString();
            });
        }

        private void StartCheckingForActivePartners()
        {
            Thread thread = new Thread(docClient.CheckIfPartnersActive);
            thread.IsBackground = true;
            thread.Start();
        }

        private void StartSendingPartnerBroadCast()
        {
            Thread thread = new Thread(broadCast.StartSendingPartnerBroadCast);
            thread.IsBackground = true;
            thread.Start();
        }

        private void ConnectDocument(object sender, RoutedEventArgs e)
        {
            docServer.Start();

            tbDocumentTitle.IsReadOnly = true;
            broadCast.DocumentTitle = tbDocumentTitle.Text;
            broadCast.Address = docServer.Address;
            StartSendingPartnerBroadCast();

            broadCast.InitialPartnerSearchSync(); // search a few seconds for partners

            // No partners found
            if (partners.Count == 0)
            {

                document = new Document(tbDocumentTitle.Text, GetDocumentAsString());
                docServer.Document = document;
                txtNrOfEditors.Text = "1";
            }
            //Found partner -> get document from first partner
            else
            {
                string address = partners[0].Address;
                docClient.GetDocument(address, UpdateDocument);
            }

            broadCast.StartFindingPartnersAsync();
        }


        //Update own document with received document
        private void UpdateDocument(DocumentMessage msg)
        {
            Application.Current.Dispatcher.Invoke(() => //execute with main thread
            {
                if (msg != null && msg.Document != null)
                {
                    document = msg.Document;

                    DocServerOnDocumentChanged(document);
                }
            });
        }

        private void DisconnectDocument(object sender, RoutedEventArgs e)
        {
            broadCast.StopFindingPartnersAsync();
            broadCast.RequestStopBroadCast();
            docServer.Dispose();

            partners.Clear();
            txtNrOfEditors.Text = "0";
            tbDocumentTitle.IsReadOnly = false;
        }

        private void tbFileName_TextChanged(object sender, TextChangedEventArgs e)
        {
            if (broadCast != null)
                broadCast.DocumentTitle = tbDocumentTitle.Text;
        }

        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            docClient.Dispose();
            docServer.Dispose();
        }



        private string defaultText = @"You can see that the code is very similar to the server. A new instance of the XcoAppSpace class is created.
Again TCP socket communication is configured, but this time no specific port is used (the client doesn't need one since no one directly wants to connect to it).
Then, the Connect() method is used to connect to the echo port that has been defined at the server. 
The only thing that is needed for that is the address of the other AppSpace instance (""myserver:8000""), the name of the port (""echo"") and the type of message the port can receive (string).
This call returns an instance of type Port<T>, where T is the message type, in this case string. By that, access to the port at the server has been established. 
This object can now be used to send messages to the server using the Post() method. The call echoPort.Post(""hello, world!"") will send the given message to the server, where it will in the end be displayed in the console window.";

    }
}
